﻿namespace VirtualTemplates
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Resources;
    using System.Web;
    using System.Web.Caching;
    using System.Web.Hosting;

    /// <summary>
    /// A virtual template that maps virtual pathes to resources of an assembly
    /// </summary>
    public class AssemblyVirtualTemplate : IVirtualTemplate
    {
        private readonly Assembly templateAssembly;

        private readonly string pathOffset;

        private readonly ResourceManager resourceManager;

        private readonly IList<VirtualDirectory> directories = new List<VirtualDirectory>();

        /// <summary>
        /// Initializes a new instance of the <see cref="AssemblyVirtualTemplate"/> class.
        /// </summary>
        /// <param name="templateAssembly">The template assembly.</param>
        /// <param name="pathOffset">The path offset.</param>
        public AssemblyVirtualTemplate(Assembly templateAssembly, string pathOffset = null)
        {
            Contract.Requires(templateAssembly != null);

            this.templateAssembly = templateAssembly;

            if (!string.IsNullOrEmpty(pathOffset))
            {
                this.pathOffset = VirtualPathUtility.AppendTrailingSlash(pathOffset);

                if (this.pathOffset.StartsWith("/"))
                {
                    this.pathOffset = pathOffset.Substring(1);
                }
            }

            this.resourceManager = new ResourceManager(templateAssembly.GetName().Name + ".g", templateAssembly) { IgnoreCase = true };

            this.InitializeDirectories();
        }

        /// <summary>
        /// Gets a value that indicates whether a file exists in the virtual file system.
        /// </summary>
        /// <param name="virtualPath">The path to the virtual file.</param>
        /// <returns>
        /// true if the file exists in the virtual file system; otherwise, false.
        /// </returns>
        public bool FileExists(string virtualPath)
        {
            return this.resourceManager.GetObject(this.ResourceNameFor(virtualPath)) != null;
        }

        /// <summary>
        /// Gets a virtual file from the virtual file system.
        /// </summary>
        /// <param name="virtualPath">The path to the virtual file.</param>
        /// <returns>
        /// A descendent of the <see cref="T:System.Web.Hosting.VirtualFile"/> class that represents a file in the virtual file system.
        /// </returns>
        public VirtualFile GetFile(string virtualPath)
        {
            return this.FileExists(virtualPath) 
                ? new AssemblyVirtualFile(this.resourceManager, this.ResourceNameFor(virtualPath), virtualPath) 
                : null;
        }

        /// <summary>
        /// Gets a value that indicates whether a directory exists in the virtual file system.
        /// </summary>
        /// <param name="virtualDir">The path to the virtual directory.</param>
        /// <returns>
        /// true if the directory exists in the virtual file system; otherwise, false.
        /// </returns>
        public bool DirectoryExists(string virtualDir)
        {
            return this.directories.Any(d => d.VirtualPath.Equals(VirtualPathUtility.ToAbsolute(virtualDir), StringComparison.InvariantCultureIgnoreCase));
        }

        /// <summary>
        /// Gets a virtual diretory from the virtual file system.
        /// </summary>
        /// <param name="virtualDir">The path to the virtual directory.</param>
        /// <returns>
        /// A descendent of the <see cref="T:System.Web.Hosting.VirtualDirectory"/> class that represents a directory in the virtual file system.
        /// </returns>
        public VirtualDirectory GetDirectory(string virtualDir)
        {
            return this.directories.FirstOrDefault(d => d.VirtualPath.Equals(VirtualPathUtility.ToAbsolute(virtualDir), StringComparison.InvariantCultureIgnoreCase));
        }

        /// <summary>
        /// Creates a cache dependency based on the specified virtual paths.
        /// </summary>
        /// <param name="virtualPath">The path to the primary virtual resource.</param>
        /// <param name="virtualPathDependencies">An array of paths to other resources required by the primary virtual resource.</param>
        /// <param name="utcStart">The UTC time at which the virtual resources were read.</param>
        /// <returns>
        /// A <see cref="T:System.Web.Caching.CacheDependency"/> object for the specified virtual resources.
        /// </returns>
        public CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
        {
            return new CacheDependency(new Uri(this.templateAssembly.CodeBase).LocalPath, utcStart);
        }

        /// <summary>
        /// Returns a hash of the specified virtual paths.
        /// </summary>
        /// <param name="virtualPath">The path to the primary virtual resource.</param>
        /// <param name="virtualPathDependencies">An array of paths to other virtual resources required by the primary virtual resource.</param>
        /// <returns>
        /// A hash of the specified virtual paths.
        /// </returns>
        public string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
        {
            var hash = new HashCode();

            foreach (var virtualPathDependency in virtualPathDependencies)
            {
                var fileInfo = new FileInfo(new Uri(this.templateAssembly.CodeBase).LocalPath);

                hash.Add(this.ResourceNameFor((string)virtualPathDependency).GetHashCode());
                hash.Add(fileInfo.FullName.GetHashCode());
                hash.Add(fileInfo.LastWriteTimeUtc.GetHashCode());
            }

            return hash.ToString();
        }

        /// <summary>
        /// Gets the resource name for the specified virtual path.
        /// </summary>
        /// <param name="virtualPath">The virtual path.</param>
        /// <returns>The resource name.</returns>
        private string ResourceNameFor(string virtualPath)
        {
            Contract.Requires(virtualPath != null);

            var relPath = VirtualPathUtility.MakeRelative("~/", virtualPath);

            if (relPath.StartsWith("/"))
            {
                relPath = relPath.Substring(1);
            }

            return this.pathOffset + relPath;
        }

        /// <summary>
        /// Initializes the directories.
        /// </summary>
        private void InitializeDirectories()
        {
            var names = this.resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true).Cast<DictionaryEntry>().Select(e => (string)e.Key);

            if (this.pathOffset != null)
            {
                names = names.Where(n => n.StartsWith(this.pathOffset, StringComparison.InvariantCultureIgnoreCase)).Select(n => n.Substring(this.pathOffset.Length));
            }

            this.InitializeDirectory(names, "/");
        }

        /// <summary>
        /// Initializes the directory.
        /// </summary>
        /// <param name="names">The resource names.</param>
        /// <param name="virtualDir">The virtual path.</param>
        /// <returns>The virtual directory.</returns>
        private AssemblyVirtualDirectory InitializeDirectory(IEnumerable<string> names, string virtualDir)
        {
            Contract.Requires(names != null);
            Contract.Requires(virtualDir != null);

            var dirs = new List<AssemblyVirtualDirectory>();
            var files = new List<AssemblyVirtualFile>();

            foreach (var group in from n in names let i = n.IndexOf('/') group n by i == -1 ? n : n.Substring(0, i))
            {
                var virtualPath = VirtualPathUtility.RemoveTrailingSlash(virtualDir) + "/" + group.Key;

                if (group.First().IndexOf('/') == -1)
                {
                    files.Add(new AssemblyVirtualFile(this.resourceManager, this.ResourceNameFor(virtualPath), virtualPath));
                }
                else
                {
                    dirs.Add(this.InitializeDirectory(group.Select(u => u.Substring(group.Key.Length + 1)).ToList(), virtualPath));
                }
            }

            var directory = new AssemblyVirtualDirectory(dirs, files, virtualDir);

            this.directories.Add(directory);
            return directory;
        }
    }
}
